/*
 * @ConfigurationManager.java
 * Copyright (c) 2010 Omnigon Communications, LLC.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of Omnigon
 * Communications, LLC ("Confidential Information"). You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with Omnigon Communications, LLC.
 *
 */
package com.perlak.configuration;

import com.perlak.logs.LoggingClass;
import java.io.File;
import java.net.URL;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.commons.codec.binary.Base64;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.EnvironmentConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;
import org.apache.commons.configuration.reloading.FileChangedReloadingStrategy;
import org.apache.commons.lang.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handles all configuration properties from one central location.
 *
 * @author gasta
 */
public final class ConfigurationManager implements LoggingClass {

  /**
   * Logger message for configuration values were not provided and using defaults.
   */
  private static final String EMPTY_CONFIG_LOG_MSG = "Configuration values were not provided "
          + "for %s, defaults were used.";
  private static final String ELEMENT_NOT_DEFINED_LOG_MSG = "Element not defined while loading "
          + "%s, using default values instead.";
  /**
   * Location of the Application XML Configuration File.
   */
  public static final String XML_LOCATION = "XML_LOCATION";
  /**
   * Property name in .properties file for the xml file location.
   */
  public static final String XML_LOCATION_PROPERTY = "fw.xml.location";
  /**
   * If the configuration cannot be found in the system environment properties
   * it will check this filename in the classpath.
   */
  public static final String DEFAULT_LOCAL_PROPERTIES_FILE =
          "base.properties";
  /**
   * Static instance.
   */
  private static ConfigurationManager instance =
          new ConfigurationManager();
  /**
   * Used for declaring that the XML config file can't be found.
   */
  private static final String CANNOT_FIND_XML_FILE = "CANNOT_FIND_XML_FILE";
  /**
   * Contains the XML configuration.
   */
  private XMLConfiguration config;
  /**
   * Main file walker settings.
   */
  private FileWalkerSettings fileWalkerSettings;
  /**
   * Holds all configuration listeners.
   */
  private final ConcurrentLinkedQueue<ConfigurationListener> listeners =
          new ConcurrentLinkedQueue<ConfigurationListener>();
  /**
   * Minimum delay between reloading configurations.
   */
  private static final int REFRESH_DELAY = 500;
  /**
   * Used to check key length.
   */
  private static final int BITS_PER_BYTE = 8;

  /**
   * Returns logger for use by static methods.
   *
   * @return The logger to by used by static methods.
   */
  private static Logger staticLog() {
    return LoggerFactory.getLogger(ConfigurationManager.class);
  }

  /**
   * Singleton implementation.
   *
   * @return The configuration.
   */
  public static ConfigurationManager getInstance() {
    return instance;
  }

  /**
   * Registers a new configuration listener.
   *
   * @param listener Registers the listener.
   */
  public void registerListener(final ConfigurationListener listener) {
    if (!listeners.contains(listener)) {
      listeners.add(listener);
    }
  }

  /**
   * Private constructor used for declaring default value configurations.
   */
  private ConfigurationManager() {
    //Fetches the default XML file location and calls the public Ctor
    this(getXmlFileLocation());
  }

  /**
   * Constructor for configuration mediator, accepts the XML
   * file location as input.
   *
   * @param xmlFile Pointer to the file location of the XML configuration file.
   */
  public ConfigurationManager(final String xmlFile) {
    getLog().debug(
            String.format("Starting public constructor for "
            + "ConfigurationManager with %s as argument.", xmlFile));

    if (!xmlFile.equals(ConfigurationManager.CANNOT_FIND_XML_FILE)) {
      loadConfigurationFromXml(xmlFile);
    }
  }

  /**
   * Returns the logger for this class.
   *
   * @return Returns a logger for this class.
   */
  @Override
  public Logger getLog() {
    return LoggerFactory.getLogger(ConfigurationManager.class);
  }

  /**
   * Returns the XML File location from either system properties or
   * configured properties.
   *
   * @return Returns the XML configuration file location.
   */
  private static String getXmlFileLocation() {
    staticLog().debug("Locating XML configuration file for "
            + "ConfigurationManager.");

    /*
     * By default we look up the XML configuration file's name from system
     * properties.
     *
     * If those don't exist we attempt to load them from a local file named
     * "registeredTraveler.properties".
     *
     * If neither of those exist we load default values.
     */
    boolean loadFromLocal = false;
    String fileName = ConfigurationManager.CANNOT_FIND_XML_FILE;

    staticLog().debug("Looking up system property for value.");
    Configuration systemConfig = new EnvironmentConfiguration();

    fileName = systemConfig.getString(ConfigurationManager.XML_LOCATION, "");

    staticLog().debug(
            String.format("The system property %s is set to %s",
            ConfigurationManager.XML_LOCATION, fileName));

    //Checking the validity of the environmental value
    if (StringUtils.isBlank(fileName)) {
      loadFromLocal = true;
      fileName = ConfigurationManager.CANNOT_FIND_XML_FILE;
    }

    if (!loadFromLocal) {
      staticLog().debug(
              String.format(
              "Returning value from environment of %s", fileName));
      return fileName;
    } else {
      //Load from local properties.
      staticLog().debug("Looking up local property for value.");

      try {
        PropertiesConfiguration pf = new PropertiesConfiguration(
                ConfigurationManager.DEFAULT_LOCAL_PROPERTIES_FILE);
        fileName = (String) pf.getProperty(
                ConfigurationManager.XML_LOCATION_PROPERTY);

        staticLog().debug(
                String.format("Loaded local property of XML file name as %s",
                fileName));

        if (StringUtils.isBlank(fileName)) {
          fileName = ConfigurationManager.CANNOT_FIND_XML_FILE;
        }
      } catch (ConfigurationException ex) {
        staticLog().warn("Configuration Exception thrown", ex);
        fileName = ConfigurationManager.CANNOT_FIND_XML_FILE;
      }
    }

    return fileName;
  }

  /**
   * Loads the configuration file from XML.
   *
   * @param xmlFile The location of the XML configuration file.
   */
  private void loadConfigurationFromXml(final String xmlFile) {
    try {
      //Load the configuration from either file or classpath.
      config = getConfiguration(xmlFile);

      FileChangedReloadingStrategy strategy =
              new FileChangedReloadingStrategy();

      strategy.setRefreshDelay(REFRESH_DELAY);

      //Set the reloading strategy.
      config.setReloadingStrategy(strategy);

      ConfigurationListener listener = new ConfigurationListener() {

        /**
         * Executed when the configuration is changed.
         * @param ce Information about the configuration changed event.
         */
        @Override
        public void configurationChanged(final ConfigurationEvent ce) {
          staticLog().info("Configuration information updated, alerting "
                  + "listeners.");
          //Configuration was updated, notify list of listeners.
          for (ConfigurationListener listener : listeners) {
            listener.configurationChanged(ce);
          }
        }
      };

      //Attach the default config changed listener that notifies all other
      //listeners.
      config.addConfigurationListener(listener);

      //Assert and set up the settings.
      configureFileWalkerSettings();

    } catch (ConfigurationException cex) {
      staticLog().error("XML File specified however it cannot"
              + "be parsed by XML Configuration.", cex);
    }
  }

  /**
   * Configures gateway settings form xml file.
   */
  private void configureFileWalkerSettings() {
    try {
      fileWalkerSettings = new FileWalkerSettings(config);
    } catch (NoSuchElementException nex) {
      getLog().warn(String.format(ELEMENT_NOT_DEFINED_LOG_MSG, "FileWalker settings"), nex);
    }
  }

  /**
   * Gets file walker main settings.
   * @return FileWalker settings
   */
  public FileWalkerSettings getSettings() {
    return fileWalkerSettings;
  }

  /**
   * Gets an XML configuration either from disk or the classloader.
   *
   * @param xmlFile
   * @param xmlFile XML file containing configuration information.
   * @return Returns the XML configuration containing key-value pairs for the
   *         application.
   * @throws ConfigurationException Thrown when there is an error loading the
   *                                configuration.
   */
  private XMLConfiguration getConfiguration(final String xmlFile)
          throws ConfigurationException {
    getLog().debug(String.format("Trying to load XML file from %s", xmlFile));

    //First try loading the file directly.
    File f = new File(xmlFile);

    if (!f.exists() || !f.canRead()) {
      getLog().debug("Trying to load from classpath since file is not readable.");
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      URL loc = loader.getResource(xmlFile);
      return new XMLConfiguration(loc);
    } else {
      return new XMLConfiguration(f);
    }
  }
}
